package pl.com.qapps.unity;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;
import java.util.Properties;
import java.util.WeakHashMap;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;

import pl.com.qapps.unity.UnityPb.ItemPb;
import pl.com.qapps.unity.UnityPb.RequestPb;
import pl.com.qapps.unity.UnityPb.ResponsePb;

/**
 * @author Jakub Dykowski
 * 
 */
final class Unity {

	// TODO do not allow access after shutdown() by throwing
	// IllegalStateException
	private static final Map<Long, Action> actions = new WeakHashMap<>();

	private static final ExecutorService eventDispatcher = Executors
			.newSingleThreadExecutor();
	private static final Properties props = new Properties();
	static {
		try {
			props.load(Unity.class.getResourceAsStream("unity.properties"));
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	static String getProperty(String name) {
		return props.getProperty(name);
	}

	private static UnityConnection bus;

	/**
	 * Once called, allocated necessary memory and made connection to Unity API.
	 * All following calls return rather the same instance, except when previous
	 * connection were broken then new one is made. The connection is shared by
	 * all services.
	 * 
	 * @return the connection bus to Unity API
	 * @throws IOException
	 *             if new connection failed to establish
	 */
	static synchronized Connection<RequestPb, ResponsePb> connect()
			throws IOException, IllegalStateException {

		if (bus == null || bus.isClosed()) {
			bus = new UnityConnection();

			// clear earlier registered items
			actions.clear();

			// if new connection is made because of error then restore all
			// quicklists of all already registered launchers
			for (UnityLauncher l : UnityLauncher.getRegisteredLaunchers()) {
				if (l != null && l.getQuicklist() != null) {
					l.setQuicklist(l.getQuicklist());
				}
			}

			// TODO restore all application indicators
		}
		return bus;
	}

	private static class UnityConnection implements
			Connection<RequestPb, ResponsePb> {

		private final ExecutorService requestDispatcher = Executors
				.newSingleThreadExecutor();
		private Thread receiver;
		private final Pipe pipe;
		private final Process process;
		private boolean closed = false;
		private BlockingQueue<ResponsePb> received = new LinkedBlockingQueue<>(
				1);

		private UnityConnection() throws IOException {
			process = new ProcessBuilder().command(
					props.get("command").toString(),
					props.get("option").toString(),
					props.get("location").toString()).start();

			InputStream in = process.getInputStream();
			int b = in.read();
			OutputStream out = process.getOutputStream();
			out.write(b);
			out.flush();
			pipe = new Pipe(out, in);

			receiver = new Thread(new Receiver());
			Runtime.getRuntime().addShutdownHook(new Thread() {

				@Override
				public void run() {
					try {
						close();
					} catch (IOException e) {
					}
				}
			});
			receiver.start();
		}

		@Override
		public ResponsePb send(final RequestPb message)
				throws IllegalStateException, IOException {

			System.out
					.println("UnityConnection: sending message: ----------------------->\n"
							+ message);
			try {
				return requestDispatcher.submit(new Callable<ResponsePb>() {

					@Override
					public ResponsePb call() throws Exception {
						pipe.send(message);
						try {
							return received.take();
						} catch (InterruptedException e) {
							e.printStackTrace(System.err);
							throw new IOException(e);
						}
					}

				}).get();
			} catch (InterruptedException | ExecutionException e) {
				throw new IOException(e);
			} finally {
				System.out.println("<--------------------------------------");
			}
		}

		// TODO consider the necessity of synchronization here
		private synchronized void close() throws IOException {
			if (!isClosed()) {
				this.closed = true;
				requestDispatcher.shutdownNow();
				receiver.interrupt();
				process.destroy();
			}
		}

		private boolean isClosed() {
			return closed;
		}

		private class Receiver implements Runnable {

			@Override
			public void run() {
				try {
					while (!closed) {
						ResponsePb res = pipe.receive(ResponsePb.newBuilder());
						if (res.hasValue() && res.getValue().hasItem()) {

							final ItemPb itemProto = res.getValue().getItem();
							final Action registered = actions.get(itemProto
									.getId());
							if (registered != null) {
								System.out.println("dispatching event for: "
										+ itemProto.getId());
								eventDispatcher.submit(registered.action);
							}
						} else {
							received.put(res);
						}
					}
				} catch (IOException | InterruptedException e) {
					System.err.println("closing unity bus due to error");
					e.printStackTrace(System.err);
					try {
						close();
					} catch (IOException consumed) {
					}
				}

			}

		}

	}

	private static class Action {

		private final Runnable action;
		private short count;

		public Action(Runnable action) {
			this.action = action;
		}
	}

	static void unregisterAction(long id) {
		synchronized (actions) {
			Action action = actions.get(id);
			if (action != null && --action.count <= 0)
				actions.remove(id);
		}
	}

	static void registerAction(long id, Runnable action) {
		if (action != null)
			synchronized (actions) {
				Action registered = actions.get(id);
				if (registered == null) {
					actions.put(id, registered = new Action(action));
				}
				registered.count++;
			}
	}

	/**
	 * Checks whether operating system supports Unity, in other words if
	 * application is running on Ubuntu and libraries necessary to access to
	 * Unity are present.
	 * 
	 * @return <code>true</code> if supported, <code>false</code> otherwise
	 */
	public static boolean isSupported() {
		// TODO check somehow if Unity is running on the OS which is of course
		// Ubuntu
		return true;
	}

	// TODO consider not exposing shutdown functionality
	public static synchronized void shutdown() {
		System.out.println("shuting down Unity");
		if (bus != null)
			try {
				bus.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		synchronized (actions) {
			actions.clear();
		}
	}

}
